<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage grove
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

/**
 *
 * Consider DataTemplate -> DataInstance?
 *
 */

namespace mg;

use Closure;
use ArrayAccess;
use ArrayObject;
use IteratorAggregate;
use ArrayIterator;
use mg\crypto\Secure;
use mg\crypto\SessionSecretToken;

final class create {

    private function __construct() { }

    /**
     * @return \mg\DataTemplate
     */
    public static function DataTemplate() {
        return new DataTemplate();
    }

    /**
     * @return \mg\TextType
     */
    public static function TextType($defaultValue = null) {
        return new TextType($defaultValue);
    }

    /**
     * @return \mg\PasswordType
     */
    public static function PasswordType() {
        return new PasswordType();
    }

    /**
     * @return \mg\TokenType
     */
    public static function TokenType() {
        return new TokenType();
    }

    /**
     * @return \mg\EmailType
     */
    public static function EmailType($defaultValue = null) {
        return new EmailType($defaultValue);
    }

    public static function UploadedFileType(array $mimeTypes = array()) {
        return new UploadedFileType($mimeTypes);
    }

    public static function CollectionType(Element $element) {
        return new CollectionType($element);
    }
}

abstract class Element {

    public $name = null;
    public $type = null;

    public function __construct() {
        $this->type = 'element';
    }

    public function setType($type) {
        $this->type = $type;
        return $this;
    }

}

abstract class CollectionElement extends Element {

    protected $elements = null;

    public function __construct() {
        parent :: __construct();

        $this->elements = array();
    }

    /**
     * @param Data $data
     * @param Validation $validation
     *
     * @return \mg\ValidatedData
     */
    public function build(Data $data = null, Validation $validation = null) {
        $isValid = true;

        if($data === null) {
            $data = new \mg\FacadeData('null', array());
            $isValid = false;
        }

        $validation = $validation ?: new FeedbackValidation(in(function(Feedback $feedback) { return $feedback; }));

        $result = array();

        /* @var $field \mg\Element */
        foreach($this->elements as $name => $element) {
            $value = isset($data[$name]) ? $data[$name] : null;

            if($element instanceof CollectionElement) {
                $value = new FacadeData($name, $value ?: array());
            }

            $validated = $element->build($value, $validation);
            $result[$name] = $validated;

            $isValid = $isValid && $validated->isValid();
        }

        $validatedData = new ValidatedData($data->getName(), $result, $this->type);
        $validatedData->isValid = $isValid;

        return $validatedData;
    }
}

class DataTemplate extends CollectionElement {

    public function __construct() {
        parent :: __construct();

        $this->type = 'template';
    }
    /**
     * @return \mg\DataTemplate
     */
    public function add($name, Element $element) {
        $name = mb_strtolower($name);
        if(!isset($this->elements[$name])) {
            $element->name = $name;
            $this->elements[$name] = $element;
        } else {
            throw new Exception("Element '{$name}' already present in DataTemplate");
        }

        return $this;
    }

}

abstract class Type extends Element {

    public $valueOnInvalid = null;

    public $defaultValue = null;

    protected $validators = null;

    protected $filter;

    protected $lastResult = null;

    public function __construct($defaultValue = null) {
        parent :: __construct();

        $this->defaultValue = $defaultValue;

        $this->valueOnInvalid = true;
        $this->validators = array();

        $this->setRequired(true);

    }

    public function isRequired() {
        return isset($this->validators['is_required']);
    }

    public function addValidator(Closure $validator) {
        $this->validators[] =  $validator;

        return $this;
    }

    public function setFilter(Closure $filter = null) {
        $this->filter = $filter;
        return $this;
    }

    public function getLastResult() {
        return $this->lastResult;
    }

    /**
     * @param $isRequired
     * @return \mg\Field
     */
    public function setRequired($isRequired) {
        if($isRequired) {
            if(!$this->isRequired()) {
                $this->validators['is_required'] = validators :: isNotNull();
            }
        } elseif(isset($this->validators['is_required'])) {
            unset($this->validators['is_required']);
        }

        return $this;
    }

    /**
     * @param $value
     * @return \mg\Field
     */
    public function setDefaultValue($defaultValue) {
        $this->defaultValue = $defaultValue;

        return $this;
    }

    // do all the hard work
    public function build($value, \mg\Validation $validation) {
        $isValid = true;

        if($this->filter !== null) {
            $filter = $this->filter;
            $value = $filter($value);
        }

        $validatedType = new ValidatedType($this->name, $value, $this->type);

        foreach($this->validators as $validator) {
            $isValid = $validation->apply($validatedType, $validator) && $isValid;
        }

        $validatedType->isValid = $isValid;

        $this->lastResult = $validatedType;

        return $validatedType;
    }

}

class validators {

    private function __construct() {

    }

    public static function isNotNull() {
        return function($value, $messages) {
            if($value === null) {
                $messages[] = g11n("validation.field.not_set");
                return false;
            }
            return true;
        };
    }

    public static function isEmailAddress() {
        return function($value, $messages) {
            if(filter_var($value, FILTER_VALIDATE_EMAIL)) {
                return true;
            }

            $messages[] = g11n("validation.email.invalid");

            return false;
        };
    }

}



class TextType extends Type {

    public function __construct() {
        parent :: __construct();

        $this->type = 'text';
    }

    /**
     * @return \mg\TextType
     */
    public function setMinimumLength($length) {
        $this->validators['minimum_length'] =
        function($value, $messages) use ($length) {
            if(mb_strlen($value) >= $length) {
                return true;
            }

            $messages[] = g11n("validation.text.too_short", mb_strlen($value), $length);

            return false;
        };

        return $this;
    }

    /**
     * @return \mg\TextField
     */
    public function setMaximumLength($length) {
        $this->validators['maximum_length'] =
        function($value, $messages) use ($length) {
            if(mb_strlen($value) <= $length) {
                return true;
            }

            $messages[] = g11n("validation.text.too_long", mb_strlen($value), $length);

            return false;
        };

        return $this;
    }

    public function setEqualTo(TextType $field) {
        $this->validators[] =
        function($value, $messages) use ($field) {
            $otherValue = $field->getLastResult();
            $otherValue = $otherValue !== null ? $otherValue->value : null;

            if($otherValue == $value) {
                return true;
            }

            $messages[] = g11n("validation.text.not_equal");

            return false;
        };

        return $this;
    }
}

class PasswordType extends TextType {

    public function __construct() {
        parent :: __construct();

        $this->type = 'password';
    }
}

class TokenType extends Type {
    public function __construct() {
        parent :: __construct();

        $this->type = 'token';

        $this->validators['valid_token'] = function($value, $messages) {
            $validToken = in(function(SessionSecretToken $secret) use($value) {
                return $secret->isValidSharedToken($value);
            });

            if(!$validToken) {
                $messages[] = g11n('validation.security.invalid_token');
                return false;
            }
        };
    }
}

class EmailType extends TextType {

    public function __construct() {
        parent :: __construct();

        $this->type = 'email';

        $this->validators['is_mail'] = validators :: isEmailAddress();

    }
}

class SetType extends Type {

    public function __construct(array $allowed = array()) {
        parent :: __construct();

        $this->type = 'set';
    }



}

class UploadedFileType extends Type {

    private $mimetypes;

    public function __construct(array $mimeTypes = array()) {
        parent :: __construct();

        $this->type = 'file';

        $this->validators = array();

        $this->validators['is_uploaded'] =
        function($value, $messages) {
            /* @var $value \mg\UploadedFile */
            if(!($value instanceof UploadedFile)) {
                $messages[] = g11n('validation.fileupload.not_uploaded');
                return false;
            }

            if($value->isSuccessfullyUploaded()) {
                return true;
            }

            $messages[] = g11n('validation.fileupload.upload_failed');

            return false;
        };

        if(count($mimeTypes) > 0) {
            $mimeTypes = array_map(function($v) { return mb_strtolower($v); }, $mimeTypes);

            $this->validators['mimetypes'] =
            function($value, $messages) use($mimeTypes) {
                /* @var $value mg\UploadedFile */
                if(!($value instanceof UploadedFile)) {
                    return false;
                }

                if(!$value->isSuccessfullyUploaded()) {
                    return false;
                }

                $mimeInfo = finfo_open(FILEINFO_MIME_TYPE);

                if(!$mimeInfo) {
                    return false;
                }

                $mimeType = finfo_file($mimeInfo, $value->getRealPath());
                	
                $result = true;

                if($mimeType === false || !in_array(mb_strtolower($mimeType), $mimeTypes)) {
                    $messages[] = g11n('validation.fileupload.invalid_filetype');
                    $result = false;
                }

                finfo_close($mimeInfo);

                return $result;
            };
        }
    }

}

class CollectionType extends CollectionElement {

    protected $element;

    public function __construct(Element $element) {
        parent :: __construct();

        $this->type = 'collection';

        $this->element = $element;
    }

    public function build(Data $data = null, Validation $validation = null) {
        $result = array();

        $isValid = true;

        foreach($data as $name=>$value) {
            if($this->element instanceof CollectionElement) {
                $value = new FacadeData($name, $value ?: array());
            }

            $this->element->name = $name;

            $validated = $this->element->build($value, $validation);
            $result[$name] = $validated;
            $isValid = $isValid && $validated->isValid();
        }

        $validatedData = new ValidatedData($data->getName(), $result, $this->type);

        $validatedData->isValid = $isValid;

        return $validatedData;
    }

}

interface Validation {

    public function apply(ValidatedType $type, Closure $validator);

}

class FeedbackValidation implements Validation {

    /**
     * @var \mg\Feedback
     */
    private $feedback = null;

    /**
     * @var \ArrayObject
     */
    private $messages = null;

    public function __construct(\mg\Feedback $feedback) {
        $this->feedback = $feedback;
        $this->messages = new ArrayObject();
    }

    public function apply(ValidatedType $validatedType, Closure $validator) {
        $messages = $this->messages;

        // Start with a clean sheet
        $messages->exchangeArray(array());

        $isValid = $validator($validatedType->value, $messages);

        if(!$isValid) {
            foreach($messages as $message) {
                $this->feedback->addErrorMessage($validatedType, $message);
            }
        }

        return $isValid;
    }

}

interface Validated {

    public function isValid();

}

class ValidatedData extends Data implements Validated {

    public $isValid = true;
    public $type = null;

    public function __construct($name, $data, $type) {
        parent :: __construct($name, $data);

        $this->type = $type;
    }

    public function isValid() {
        return $this->isValid;
    }

    public function __get($name) {
        return $this[$name];
    }

    public function __isset($name) {
        return isset($this[$name]);
    }
    public function getData($name) {
        return null;
    }
}

class ValidatedType extends MixedType implements Validated {

    public $name = null;
    public $value = null;
    public $type = null;
    public $isValid = true;

    public function __construct($name, $value, $type) {
        parent :: __construct($value);

        $this->name = $name;
        $this->type = $type;
    }

    public function isValid() {
        return $this->isValid;
    }

}
